package org.keyintegrity.webbeans.metadata.store.xmlstream;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import org.apache.log4j.Logger;
import org.keyintegrity.webbeans.metadata.BeanMetadata;
import org.keyintegrity.webbeans.metadata.store.AbstractStore;
import org.keyintegrity.webbeans.metadata.store.MetadataNotFoundException;
import org.keyintegrity.webbeans.metadata.store.MetadataStore;
import org.keyintegrity.webbeans.metadata.store.MetadataStoreException;
import org.keyintegrity.webbeans.metadata.store.xmlstream.XmlStreamStoreContext.Scope;
import org.keyintegrity.webbeans.utils.BeanUtils;

@XmlRootElement
public class XmlStreamStore extends AbstractStore {

    /**
     * 
     */
    private static final long serialVersionUID = -9167849865961558512L;

    private static final Logger logger = Logger.getLogger(MetadataStore.class);
    
    private List<XmlBeanMetadata> xmlMetadataList;
    
    private String context;

    private Scope scope;

    private XmlStreamStore() {}
    
    public void setContext(String context) {
        this.context = context;
    }
    
    public void setScope(Scope scope) {
        this.scope = scope;
    }

    @XmlTransient
    public List<BeanMetadata> getMetadataList() {
        return new ArrayList<BeanMetadata>(getXmlMetadataList());
    }

    @XmlElement
    public List<XmlBeanMetadata> getXmlMetadataList() {
        if (xmlMetadataList == null) {
            xmlMetadataList = new ArrayList<XmlBeanMetadata>();
        }
        return xmlMetadataList;
    }

    public void clear() {
        getXmlMetadataList().clear();
    }

    /**
     * Find metadada file by class name
     * 
     * {@inheritDoc}
     */
    public BeanMetadata loadMetadata(String clazz) throws MetadataNotFoundException {
        for (BeanMetadata metadata : getMetadataList()) {
            if (metadata.getAssociatedClass().equals(clazz)) {
                return metadata;
            }
        }
        throw new MetadataNotFoundException(clazz);
    }

    public void removeMetadata(BeanMetadata metadata) {
        super.removeMetadata(metadata);
        getXmlMetadataList().remove(metadata);
    }

    public void storeMetadata(BeanMetadata metadata) throws MetadataStoreException {
        File outputFile;
        
        switch (scope) {
        case RUNTIME:

            logger.warn("Can't change webbeans core settings at runtime. They're considered to be constant.");
            return;

        case BUILD:
            
            outputFile = new File("src/main/resources/" + getStoreContextResourceName(context));
            
            break;
        
        default /* TEST */:
            
            outputFile = new File("target/test-classes/" + getStoreContextResourceName(context));
            
            break;
        }

        updateXmlMetadataList(metadata);

        if (! outputFile.exists()) {
            try {
                outputFile.getParentFile().mkdirs();
                outputFile.createNewFile();
                if (logger.isDebugEnabled()) {
                    logger.debug("Created metadata store : " + outputFile.getAbsolutePath());
                }
            } catch (IOException e) {
                throw new RuntimeException("Can't create metadata store : " + outputFile.getAbsolutePath());
            }
        }
        try {
            saveToStream(new FileOutputStream(outputFile));
        } catch (FileNotFoundException e) {
            new MetadataStoreException(e.getMessage(), e);
        }
    }

    private void updateXmlMetadataList(BeanMetadata metadata) {
        for (BeanMetadata bm : getMetadataList()) {
            if (bm.getAssociatedClass().equals(metadata.getAssociatedClass())) {
                //  Replace with new metadata
                getXmlMetadataList().remove(bm);
            }
        }
        getXmlMetadataList().add(new XmlBeanMetadata(metadata));
    }

    public static String getStoreContextResourceName(String context) {
        return (context.endsWith("/") ? context 
                : context + "/") + "XmlStreamStore.xml";
    }
    
    /** 
     * Stub for {@link BeanUtils#getBeanFieldNames(Class)} so that it 
     * can find setter for {@link #getMetadataList()} and treat them as bean field 
     */
    public void setMetadataList(List<BeanMetadata> metadataList) {
        //  Nothing
    }

    @Override
    public String toString() {
        return MessageFormat.format("Xml Stream Store (classpath:/{0})", getStoreContextResourceName(context));
    }

    public static XmlStreamStore loadFromStream(InputStream inputStream) throws MetadataStoreException {
        try {
            JAXBContext context = JAXBContext.newInstance(XmlStreamStore.class);
            Unmarshaller um = context.createUnmarshaller();
            return (XmlStreamStore) um.unmarshal(inputStream);
        } catch (JAXBException e) {
            throw new MetadataStoreException(e.getMessage(), e);
        }
    }
    
    public void saveToStream(OutputStream outputStream) throws MetadataStoreException {
        try {
            JAXBContext context = JAXBContext.newInstance(XmlStreamStore.class);
            Marshaller m = context.createMarshaller();
            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); 
            m.marshal(this, outputStream);
        } catch (Exception e) {
            throw new MetadataStoreException(e.getMessage(), e);
        }
    }
    
    @Override
    public BeanMetadata newBeanMetadata() {
        return new XmlBeanMetadata();
    }
}
